Bank Marketing and Customer Relationship Management: A Case Study¶

How to Attract and Retain Customers with Term Deposits¶


Bank marketing is a crucial strategy for any financial institution that wants to stand out from the competition and build a loyal customer base. Bank marketing involves creating a unique brand image, offering innovative products and services, and delivering quality and value to the customers.

One of the products that banks can use to attract and retain customers is term deposits. Term deposits are deposit accounts that have a fixed maturity date and a fixed interest rate. Term deposits are appealing to customers who want to save money for a specific goal or who want to earn a higher interest rate than a regular savings account.

Term deposits also benefit the banks, as they provide a stable source of funding and reduce the risk of liquidity problems. Term deposits can help banks to diversify their portfolio and manage their interest rate risk. Term deposits can also enhance the bank's reputation and customer relationship, as they show that the bank values the customer's trust and loyalty.

However, term deposits also pose some challenges for both the customers and the banks. Customers may face penalties or fees if they withdraw their money before the maturity date. Customers may also lose out on better interest rates or investment opportunities if the market conditions change. Banks may face the risk of losing customers if they fail to offer competitive interest rates or renewals. Banks may also have to deal with mismatched assets and liabilities if the term deposits have different maturities than the loans they fund.

Therefore, bank marketing and customer relationship management are essential for ensuring the success of term deposits. Bank marketing should focus on highlighting the benefits of term deposits, such as security, stability, and guaranteed returns. Bank marketing should also target the right customers, such as those who have a low-risk appetite, a long-term saving horizon, or a specific financial goal. Bank marketing should also use various channels, such as online platforms, social media, or direct mail, to reach out to potential and existing customers.

Customer relationship management should focus on providing excellent service and support to the term deposit holders. Customer relationship management should also involve offering personalized advice and solutions, such as flexible terms, attractive rates, or value-added features. Customer relationship management should also aim to retain the customers after the term deposit matures, by offering them other products or services that suit their needs and preferences.

Term deposits are a valuable tool for bank marketing and customer relationship management. By using term deposits effectively, banks can increase their customer base, enhance their brand image, and improve their profitability. 


The business goal of this project is to reduce the marketing resources by identifying the customers who would be interested in subscribing to a term deposit. By doing so, the marketing team can focus their efforts on the most promising leads and increase the conversion rate. This will also improve the customer satisfaction and loyalty, as they will receive more relevant and personalized offers.



Attribute Description
age numeric
job type of job (categorical: "admin.", "unknown", "unemployed", "management", "housemaid", "entrepreneur", "student", "blue-collar", "self-employed", "retired", "technician", "services")
marital marital status (categorical: "married", "divorced", "single"; note: "divorced" means divorced or widowed)
education categorical: "unknown", "secondary", "primary", "tertiary"
default has credit in default? (binary: "yes", "no")
balance average yearly balance, in euros (numeric)
housing has housing loan? (binary: "yes", "no")
loan has personal loan? (binary: "yes", "no")
contact contact communication type (categorical: "unknown", "telephone", "cellular")
day last contact day of the month (numeric)
month last contact month of year (categorical: "jan", "feb", "mar", ..., "nov", "dec")
duration last contact duration, in seconds (numeric)
campaign number of contacts performed during this campaign and for this client (numeric, includes last contact)
pdays number of days that passed by after the client was last contacted from a previous campaign (numeric, -1 means client was not previously contacted)
previous number of contacts performed before this campaign and for this client (numeric)
poutcome outcome of the previous marketing campaign (categorical: "unknown", "other", "failure", "success")


Importing Libraries¶

In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px
import seaborn as sns
import datetime as dt
import plotly.graph_objects as go
import warnings
warnings.filterwarnings('ignore')

Importing Data¶

In [2]:
df = pd.read_csv('bank.csv')
In [3]:
df.head()
Out[3]:
age job marital education default balance housing loan contact day month duration campaign pdays previous poutcome deposit
0 59 admin. married secondary no 2343 yes no unknown 5 may 1042 1 -1 0 unknown yes
1 56 admin. married secondary no 45 no no unknown 5 may 1467 1 -1 0 unknown yes
2 41 technician married secondary no 1270 yes no unknown 5 may 1389 1 -1 0 unknown yes
3 55 services married secondary no 2476 yes no unknown 5 may 579 1 -1 0 unknown yes
4 54 admin. married tertiary no 184 no no unknown 5 may 673 2 -1 0 unknown yes
In [4]:
df.columns
Out[4]:
Index(['age', 'job', 'marital', 'education', 'default', 'balance', 'housing',
       'loan', 'contact', 'day', 'month', 'duration', 'campaign', 'pdays',
       'previous', 'poutcome', 'deposit'],
      dtype='object')
In [5]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 11162 entries, 0 to 11161
Data columns (total 17 columns):
 #   Column     Non-Null Count  Dtype 
---  ------     --------------  ----- 
 0   age        11162 non-null  int64 
 1   job        11162 non-null  object
 2   marital    11162 non-null  object
 3   education  11162 non-null  object
 4   default    11162 non-null  object
 5   balance    11162 non-null  int64 
 6   housing    11162 non-null  object
 7   loan       11162 non-null  object
 8   contact    11162 non-null  object
 9   day        11162 non-null  int64 
 10  month      11162 non-null  object
 11  duration   11162 non-null  int64 
 12  campaign   11162 non-null  int64 
 13  pdays      11162 non-null  int64 
 14  previous   11162 non-null  int64 
 15  poutcome   11162 non-null  object
 16  deposit    11162 non-null  object
dtypes: int64(7), object(10)
memory usage: 1.4+ MB

Exploratory Data Analysis¶

In [6]:
sns.histplot(x=df['age'], label='Age', color='skyblue', kde=True)
plt.axvline(x=df['age'].mean(), color='red', linestyle="--", label='Mean Age: {:.2f}'.format(df['age'].mean()))
plt.legend()
plt.title('Distribution of Age')
plt.show()
In [7]:
sns.kdeplot(x=df['duration'], color='skyblue', shade=True, label='Duration')
plt.axvline(x=df['duration'].mean(), color='red', linestyle="--", label='Mean Duration: {:.2f}'.format(df['duration'].mean()))
plt.legend()
plt.title('Distribution of Duration')
plt.show()
In [8]:
sns.kdeplot(x=df['pdays'], color='skyblue', shade=True, label='Duration')
plt.axvline(x=df['pdays'].mean(), color='red', linestyle="--", label='Mean Duration: {:.2f}'.format(df['duration'].mean()))
plt.legend()
plt.title('Distribution of pdays')
plt.show()
In [9]:
fig = px.bar(
    df.job.value_counts().reset_index(),
    x='job',
    y='count',
    color='job',
    text='count',
)
fig.update_layout(

    title_text='Job Type of the Customers'
)
In [10]:
fig = px.pie(
    df.marital.value_counts().reset_index(),
    names='marital',
    values='count'
)

fig.update_layout(
    showlegend=True,
    legend_title_text='Marital Status'
)
fig.update_traces(textposition='outside', textinfo='percent+label')
fig.update_layout(

    legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
)
fig.show()
In [11]:
fig = px.bar(
    df.education.value_counts().reset_index().rename(columns={'education': 'Education', 'count': 'Count'}),
    x='Education',
    y='Count',
    color='Education',
    text='Count',
)

fig.update_layout(
    legend_title_text='Education',
    title_text='Education of the Customers'
)
fig.show()

fig = px.pie(
    df.education.value_counts().reset_index().rename(columns={'education': 'Education', 'count': 'Count'}),
    names='Education',
    values='Count',
)

fig.update_layout(
    showlegend=True,
    legend_title_text='Education'
)
fig.update_traces(textposition='outside', textinfo='percent+label')
fig.update_layout(
    legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
)
fig.show()
In [12]:
from plotly.subplots import make_subplots

fig = make_subplots(
    rows=1,
    cols=3,
    specs=[[{"type": "pie"}, {"type": "pie"}, {"type": "pie"}]],
    subplot_titles=('Credit in Default', 'Housing Loan', 'Personal Loan')
)

default_counts = df.default.value_counts().reset_index().rename(columns={'default': 'Default', 'count': 'Count'})
fig.add_trace(
    go.Pie(
        values=default_counts['Count'],
        labels=default_counts['Default'],
        name='Has Credit in Default?',
        showlegend=False
    ),
    row=1,
    col=1
)

housing_counts = df.housing.value_counts().reset_index().rename(columns={'housing': 'Housing', 'count': 'Count'})
fig.add_trace(
    go.Pie(
        values=housing_counts['Count'],
        labels=housing_counts['Housing'],
        name='Housing Loan',
        showlegend=False
    ),
    row=1,
    col=2
)

loan_counts = df.loan.value_counts().reset_index().rename(columns={'loan': 'Loan', 'count': 'Count'})
fig.add_trace(
    go.Pie(
        values=loan_counts['Count'],
        labels=loan_counts['Loan'],
        name='Personal Loan',
        showlegend=True
    ),
    row=1,
    col=3
)
fig.update_layout(
    showlegend=True,
    legend_title_text=" ",
    title_text='Credit and Loan Analysis'
)
In [13]:
fig = px.pie(
    df.contact.value_counts().reset_index().rename(columns={'contact': 'Contact', 'count': 'Count'}),
    names='Contact',
    values='Count'
)
fig.update_layout(
    showlegend=True,
    legend_title_text='Contact'
)
fig.update_traces(textposition='outside', textinfo='percent+label')
fig.update_layout(
    legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
)
fig.show()
In [14]:
fig = px.bar(
    df.month.value_counts().reset_index().rename(columns={'month': 'Month', 'count': 'Count'}),
    x='Month',
    y='Count',
    color='Month',
    text='Count',
)
fig.update_layout(
    title_text='Last Contact Month of the Year'   
)
In [15]:
fig = px.bar(
    df.poutcome.value_counts().reset_index().rename(columns={'poutcome': 'Poutcome', 'count': 'Count'}),
    x='Poutcome',
    y='Count',
    color='Poutcome',
    text='Count',
)

fig.update_layout(
    title_text='Outcome of Previous Marketing Campaign',
)
fig.show()

fig = px.pie(
    df.poutcome.value_counts().reset_index().rename(columns={'poutcome': 'Poutcome', 'count': 'Count'}),
    names='Poutcome',
    values='Count',
)
fig.update_layout(
    showlegend=True,
    legend_title_text='Poutcome'
)
fig.update_traces(textposition='outside', textinfo='percent+label')
fig.update_layout(
    title_text='Outcome of Previous Marketing Campaign'
)
fig.update_layout(
    legend=dict(orientation='v', yanchor='middle', y=1.02, xanchor='right', x=1)
)
fig.show()
In [16]:
fig = px.pie(
    df.groupby(['deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'}),
    names='deposit',
    values='Count'
)

fig.update_layout(
    showlegend=True,
    legend_title_text='Deposit'
)
fig.update_traces(textposition='outside', textinfo='percent+label')
fig.update_layout(
    title_text='Term Deposits',
)
fig.update_layout(
    legend=dict(orientation='v', yanchor='middle', y=1.02, xanchor='right', x=1)
)
fig.show()
In [17]:
a = df.groupby(['job'], as_index=False)['balance'].mean()
a['balance'] = round(a['balance'], 1)
fig = px.bar(
    a.sort_values(by='balance', ascending=False),
    x='job',
    y='balance',
    text='balance',
    color='job',
)

fig.update_layout(
    title_text='Average Balance of the Clients by Their Job Type',
    legend_title_text='Job Type'
)
In [18]:
fig = px.box(
    df,
    x='job',
    y='age',
    color='job',
    title='Distribution of Age Based on Job Type'
)
fig.update_layout(
    legend_title_text="Job Type"
)
In [19]:
fig = px.box(
    df,
    x='marital',
    y='age',
    color='marital',
    title='Distribution of Age Based on Marital Status'
)
fig.update_layout(
    legend_title_text="Marital Status"
)
fig.show()
In [20]:
fig=px.box(
    df,x='education',
    y='age',
    color='education',
    title='Distribution of Age Based on Education Level')
fig.update_layout(
    legend_title_text="Education Level")
fig.show()
In [21]:
fig=px.box(
    df,x='housing',
    y='age',
    color='housing',
    title='Distribution of age based on Housin Loan Status'
)
fig.update_layout(
    legend_title_text="Housing Loan"
)
fig.show()
In [22]:
fig=px.box(
    df,x='loan',
    y='age',
    color='loan',
    title='Distribution of age based on Personal Loan Status'
)
fig.update_layout(
    legend_title_text="Personal Loan"
)
fig.show()
In [23]:
fig=px.box(
    df,x='deposit',
    y='age',
    color='deposit',
    title='Distribution of age based on Term Deposit Status'
)
fig.update_layout(
    legend_title_text="Term Deposit"
)
fig.show()
In [24]:
fig=px.box(
    df,x='deposit',
    y='pdays',
    color='deposit',
    title='Distribution of pdays based on Term Deposit Status'
)
fig.update_layout(
    legend_title_text="Term Deposit"
)
fig.show()
In [25]:
fig=px.box(
    df,x='poutcome',
    y='pdays',
    color='poutcome',
    title='Effect of pdays on poutcome'
)
fig.update_layout(
    legend_title_text="Credit in Default"
)
fig.update_layout(
    legend_title_text="poutcome"
)
fig.show()
In [26]:
fig=px.box(
    df,x='poutcome',
    y='pdays',
    color='deposit',
    title='Effect of pdays on poutcome and term deposits'
)
fig.update_layout(
    legend_title_text="Deposit"
)
fig.show()
In [27]:
fig=px.box(
    df,x='deposit',
    y='duration',
    color='deposit'
    ,title='Effect of Duration on Term Deposit Status'
)
fig.update_layout(
    legend_title_text="Term Deposit"
)
fig.show()
In [28]:
df['Duration'] = df['duration'].apply(lambda x: 'Above_Average_Duration' if x >= df['duration'].mean() else 'Below_Average_Duration')
a = df.groupby(['Duration', 'deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'})
a['percent'] = round(a['Count'] * 100 / a.groupby('Duration')['Count'].transform('sum'), 1)
a['percent'] = a['percent'].apply(lambda x: '{}%'.format(x))
fig = px.bar(a, x='Duration', y='Count', text='percent', color='deposit', barmode='group')
fig.show()
In [29]:
df['Balance'] = df['balance'].apply(lambda x: 'Above_Average' if x >= df['balance'].mean() else 'Below_Average')
a = df.groupby(['Balance', 'deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'})
a['percent'] = round(a['Count'] * 100 / a.groupby('Balance')['Count'].transform('sum'), 1)
a['percent'] = a['percent'].apply(lambda x: '{}%'.format(x))
fig = px.bar(a, x='Balance', y='Count', text='percent', color='deposit', barmode='group')
fig.update_layout(legend_title_text='Deposit Status:', title_text='Effect of Balance on Term Deposits')
fig.update_traces(textposition='outside')
fig.show()
In [30]:
k = df.groupby(['housing', 'deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'})
fig = make_subplots(rows=1, cols=2, specs=[[{"type": "pie"}, {"type": "pie"}]])
fig.add_trace(go.Pie(values=k[k['housing'] == 'yes']['Count'], labels=k[k['housing'] == 'yes']['deposit'], hole=0.7, name='Having Housing Loan', showlegend=False), row=1, col=1)
fig.add_trace(go.Pie(values=k[k['housing'] == 'no']['Count'], labels=k[k['housing'] == 'no']['deposit'], hole=0.7, name='Not having Housing loan', showlegend=True), row=1, col=2)
fig.update_layout(title_x=0.5, showlegend=True, legend_title_text="Deposit", title_text='Term Deposits based on Housing Loan')
fig.add_annotation(dict(x=0.15, y=0.5, align='center', xref="paper", yref="paper", showarrow=False, font_size=22, text="Have a Loan"))
fig.add_annotation(dict(x=0.83, y=0.5, align='center', xref="paper", yref="paper", showarrow=False, font_size=22, text="No Loan"))
fig.update_traces(textposition='outside', textinfo='percent+label')
In [31]:
k = df.groupby(['loan', 'deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'})
fig = make_subplots(rows=1, cols=2, specs=[[{"type": "pie"}, {"type": "pie"}]])
fig.add_trace(go.Pie(values=k[k['loan'] == 'yes']['Count'], labels=k[k['loan'] == 'yes']['deposit'], hole=0.7, name='Having Personal Loan', showlegend=False), row=1, col=1)
fig.add_trace(go.Pie(values=k[k['loan'] == 'no']['Count'], labels=k[k['loan'] == 'no']['deposit'], hole=0.7, name='Not having Personal loan', showlegend=True), row=1, col=2)
fig.update_layout(title_x=0.5, showlegend=True, legend_title_text="Deposit", title_text='Term Deposits based on Personal Loan')
fig.add_annotation(dict(x=0.15, y=0.5, align='center', xref="paper", yref="paper", showarrow=False, font_size=22, text="Have a Loan"))
fig.add_annotation(dict(x=0.83, y=0.5, align='center', xref="paper", yref="paper", showarrow=False, font_size=22, text="No Loan"))
fig.update_traces(textposition='outside', textinfo='percent+label')
In [32]:
d = df.groupby(['marital', 'deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'})
fig = make_subplots(rows=1, cols=3, specs=[[{"type": "pie"}, {"type": "pie"}, {"type": "pie"}]], subplot_titles=('Divorced', 'Married', 'Single'))
fig.add_trace(go.Pie(values=d[d['marital'] == 'divorced']['Count'], labels=d[d['marital'] == 'divorced']['deposit'], name='Divorced', showlegend=False), row=1, col=1)
fig.add_trace(go.Pie(values=d[d['marital'] == 'married']['Count'], labels=d[d['marital'] == 'married']['deposit'], name='Married', showlegend=False), row=1, col=2)
fig.add_trace(go.Pie(values=d[d['marital'] == 'single']['Count'], labels=d[d['marital'] == 'single']['deposit'], name='Single', showlegend=True), row=1, col=3)
fig.update_layout(title_x=0.5, showlegend=True, legend_title_text="Deposit", title_text='Term Deposits based on Marital Status')
fig.update_traces(textposition='inside', textinfo='percent')
In [33]:
a = df.groupby(['job', 'deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'})
a['percent'] = round(a['Count'] * 100 / a.groupby('job')['Count'].transform('sum'), 1)
a['percent'] = a['percent'].apply(lambda x: '{}%'.format(x))
fig = px.bar(a, x='job', y='Count', text='percent', color='deposit', barmode='group')
fig.update_layout(title_x=0.5, showlegend=True, title_text='Term Depositors based on Job Type')
fig.show()
In [34]:
fig = px.line(df.groupby(['age', 'loan'], as_index=False)['job'].count().rename(columns={'job': 'Count'}), x='age', y='Count', color='loan')
fig.update_layout(title_x=0.5, legend_title_text="Term Deposit", title_text='Effect of Age on Personal Loan')
fig.show()
In [35]:
fig = px.line(df.groupby(['age', 'housing'], as_index=False)['job'].count().rename(columns={'job': 'Count'}), x='age', y='Count', color='housing')
fig.update_layout(title_x=0.5, legend_title_text="Term Deposit", title_text='Effect of Age on Housing Loan')
fig.show()
In [36]:
fig = px.line(df.groupby(['age', 'deposit'], as_index=False)['job'].count().rename(columns={'job': 'Count'}), x='age', y='Count', color='deposit')
fig.update_layout(title_x=0.08, legend_title_text="Term Deposit", title_text='Effect of Age on Term Deposits')
fig.show()
In [37]:
a = df.groupby(['month', 'deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'})
a['percent'] = round(a['Count'] * 100 / a.groupby('month')['Count'].transform('sum'), 1)
a['percent'] = a['percent'].apply(lambda x: '{}%'.format(x))
fig = px.bar(a, x='month', y='Count', text='percent', color='deposit', barmode='group')
fig.update_layout(title_x=0.5, showlegend=True, legend_title_text="Deposit", title_text='Deposits based on last Contact month')
fig.show()
In [38]:
fig = px.line(df.groupby(['campaign', 'deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'}), x='campaign', y='Count', color='deposit')
fig.update_layout(title_x=0.08, legend_title_text="Term Deposit", title_text='Effect of Campaign on Term Deposits')
fig.show()
In [39]:
fig = px.line(df.groupby(['previous', 'deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'}), x='previous', y='Count', color='deposit')
fig.update_layout(title_x=0.08, legend_title_text="Term Deposit", title_text='Effect of Previous Campaign on Term Deposits')
fig.show()
In [40]:
a = df.groupby(['poutcome', 'deposit'], as_index=False)['age'].count().rename(columns={'age': 'Count'})
a['percentile'] = round(a['Count'] * 100 / a.groupby('poutcome')['Count'].transform('sum'), 1)
a['percentile'] = a['percentile'].apply(lambda x: '{}%'.format(x))
fig = px.bar(a, x='poutcome', y='Count', color='deposit', text='percentile', barmode='group')
fig.update_layout(title_x=0.08, showlegend=True, legend_title_text="Deposit", title_text='Term Deposits based on Outcome of Previous Marketing Campaign')
fig.show()

Conclusion¶

  • Most of the clients in the bank are contacted in the months of May, Jun, Jul and in Aug last year. Out of that, most of the clients contacted in the month of May and also this is the month where clients are not interested to subscribe to the term deposits. Very few of the clients are contacted in the months of Mar, Sep and in Dec. It is better to contact the clients more in these months.
  • If multiple campaigns and multiple contacts are performed for the clients, there is more chance for the client to not be interested to subscribe for the term deposit. At most 2 or 3 contacts can be preferred to perform for the clients.
  • Occupation of the clients in retired, student and housemaid are subscribed more to the term deposits. Retired individuals tend to not spend bigly its cash so they are more likely to put their cash to work by lending it to the financial institution. Students were the other group that used to subscribe term deposits.
  • From the analysis, even though the mid-age group clients subscribed more for the term deposits, there is a more domination of clients from this group who are not subscribed for the term deposits. Old age group clients are subscribing the term deposits more and count of not subscribing to the deposits is less. It is well for the banks to focus on old age group clients to get more subscriptions for the term deposits.
  • From the outcome of previous campaign, if the outcome is failure, then there is a 50% chance that the client will not subscribe to the term deposit. Out of all failure outcomes 50.3% of clients subscribe and 49.7% are not subscribed to the term deposits.
  • From the outcome of previous campaign, if the outcome is success, then there is a high chance that the client will subscribe to the term deposit. Out of all success outcomes 91.3% of clients subscribe and 8.7% are not subscribed to the term deposits.
  • Clients who are not interested to take housing loans may be interested in subscribing for the term deposits. If the balance of client is above average balance then they are more likely to subscribe for the term deposits.